package client;

import etc.SynchronizationParameters;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.JOptionPane;
import lib.Base64;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;

public class SyncClient {

	private Socket socket = null;

	private BufferedWriter out = null;

	private BufferedReader in = null;

	private int chunkSize = 0;

	public String connect() {
		try{
			this.socket = new Socket( SynchronizationParameters.getRemoteIP(), SynchronizationParameters.getRemotePort() );
			// Dividing the available buffer size by 2, just to be safe...
			this.chunkSize = socket.getSendBufferSize() / 2;
			this.out = new BufferedWriter( new OutputStreamWriter(this.socket.getOutputStream() ) );
			this.in = new BufferedReader( new InputStreamReader(this.socket.getInputStream() ) );
			return "Connected to remote host " + socket.getRemoteSocketAddress().toString();
		}catch(Exception e) {
			return "Could not connect to remote host ! Reason: "+e.getLocalizedMessage();
		}
	}

	public String disconnect() {
		try {
			if(socket != null) socket.close();
		} catch (IOException ex) {}

		return "Disconnected from remote host.";
	}

	public String login() {
		if( socket.isConnected() ) {
			try{
				writeLine("<login username='" + SynchronizationParameters.getRemoteUsername()
						+ "' password='" + SynchronizationParameters.getRemotePassword()
						+ "' />");

				Element rootElement = readXMLLine();
				if( rootElement.getName().equalsIgnoreCase("login") ) {
					String result = readXMLResponseResult(rootElement);
					if( result != null ) {
						if( result.equalsIgnoreCase("OK") ) {
							return "Authorized on remote host at " + socket.getRemoteSocketAddress().toString();
						}else{
							return "Could not authorize on remote host! Response: " + rootElement.getText();
						}
					}
				}else{
					return "Malformed server response to login request. (Received '" + rootElement.getName() + "' instead of login response)";
				}
			}catch(Exception e) {
				return "Could not login. Exception: " + e.getLocalizedMessage();
			}
		}

		return "Not connected to remote host";
	}

	public String logout() {
		if( socket.isConnected() ) {
			try{
				writeLine("<logout />");

				Element rootElement = readXMLLine();
				if( rootElement.getName().equalsIgnoreCase("logout") ) {
					String result = readXMLResponseResult(rootElement);
					if( result != null ) {
						if( result.equalsIgnoreCase("OK") ) {
							return "Logged out from  remote host at " + socket.getRemoteSocketAddress().toString();
						}else{
							return "Could not log out from remote host! Response: " + rootElement.getText();
						}
					}
				}else{
					return "Malformed server response to logout request. (Received '" + rootElement.getName() + "' instead of logout response)";
				}
			}catch(Exception e) {
				return "Could not logout. Exception: " + e.getLocalizedMessage();
			}
		}

		return "Not connected to remote host";
	}

	public String deleteRemoteFile( String fileName ) {
		if( socket.isConnected() ) {
			try{
				writeLine("<delete file='"+fileName+"' />");

				Element rootElement = readXMLLine();
				if( rootElement.getName().equalsIgnoreCase("delete") ) {
					String result = readXMLResponseResult(rootElement);
					if( result != null ) {
						if( result.equalsIgnoreCase("OK") ) {
							return "Deleted remote file " + fileName;
						}else{
							return "Could not delete remote file " + fileName + ". Response: " + rootElement.getText();
						}
					}
				}else{
					return "Malformed server response to delete request. (Received '" + rootElement.getName() + "' instead of delete response)";
				}
			}catch(Exception e) {
				return "Could not delete. Exception: " + e.getLocalizedMessage();
			}
		}

		return "Not connected to remote host";
	}

	public String downloadFile( String fileName, Long fileSize ) {
		if( socket.isConnected() ) {
			try{
				writeLine("<download file='"+fileName+"' size='"+fileSize.toString()+"' />");

				Element rootElement = readXMLLine();
				if( rootElement.getName().equalsIgnoreCase("upload") ) {
					if( rootElement.attributeValue("file").equalsIgnoreCase(fileName)
						&& rootElement.attributeValue("size").equalsIgnoreCase(fileSize.toString()) ) {

						String lastModifiedAttr = rootElement.attributeValue("lastModified");
						Long lastModified = null;
						if( lastModifiedAttr != null) {
							try{
								lastModified = Long.parseLong( lastModifiedAttr );
							}catch(Exception e){}
						}

						File file = new File( SynchronizationParameters.getLocalPath() + "\\" + fileName.replace('/', '\\') );
						file.getParentFile().mkdirs();

						FileOutputStream outToFile = new FileOutputStream( file );
						String retMsg = "";
						long waitingBytes = fileSize;
						while( waitingBytes > 0 ) {
							writeLine("<waiting size='"+Long.toString( waitingBytes )+"' file='"+fileName+"' />");

							rootElement = readXMLLine();
							if( rootElement.getName().equalsIgnoreCase("data") ) {
								String base64EncodedString = rootElement.getText();
								byte[] decodedData = Base64.decode( base64EncodedString );

								outToFile.write( decodedData );

								long bytesInPackage = (long)decodedData.length;
								if( bytesInPackage == waitingBytes ) {
									writeLine("<receive didFail='false' file='"+fileName+"' size='"+fileSize.toString()+"' />");
									retMsg = "Downloaded file '"+fileName+"'.";

									outToFile.flush();
									outToFile.close();

									if( lastModified != null ) {
										file.setLastModified( lastModified );
									}

									break;
								}else if( bytesInPackage > waitingBytes ) {
									writeLine("<receive didFail='true' file='"+fileName+"' size='"+fileSize.toString()+"' />");
									retMsg = "Could not download file '"+fileName+"', incoming bytes greater than expected bytes.";

									outToFile.flush();
									outToFile.close();
									file.delete();

									break;
								}else if( bytesInPackage < waitingBytes ) {
									waitingBytes -= bytesInPackage;
								}
							}else{
								writeLine("<receive didFail='true' file='"+fileName+"' size='"+fileSize.toString()+"' />");
								return "Malformed server response to download request. (Received '" + rootElement.getName() + "' instead of download response)";
							}
						}

						file = null;

						return retMsg;
					}else{
						return "Download of file '"+fileName+"' failed, remote and local properties mismatch.";
					}
				}else{
					return "Download of file '"+fileName+"' failed, server sent '"+rootElement.getName()+"' instead of an upload response.";
				}
			}catch(Exception e) {
				return "Could not download '"+fileName+"'. Exception: " + e.getLocalizedMessage();
			}
		}else{
			return "Not connected to remote host";
		}
	}

	public String uploadFile( String fileName ) {
		if( socket.isConnected() ) {
			try{
				File sourceFile = new File( SynchronizationParameters.getLocalPath() + "\\" + fileName.replace('/', '\\') );
				FileInputStream inFromFile = new FileInputStream( sourceFile );

				if( (!sourceFile.exists()) || (inFromFile == null) ) {
					return "Could not upload '"+fileName+"'. Unable to read file.";
				}

				Long fileSize = sourceFile.length();
				Long lastModified = sourceFile.lastModified();

				writeLine("<upload file='"+fileName+"' size='"+fileSize.toString()+"' lastModified='"+lastModified.toString()+"'/>");

				Element rootElement = readXMLLine();
				if( rootElement.getName().equalsIgnoreCase("waiting") ) {
					if( rootElement.attributeValue("file").equalsIgnoreCase(fileName)
						&& rootElement.attributeValue("size").equalsIgnoreCase(fileSize.toString()) ) {

						String retMsg = "";
						long waitingBytes = fileSize;
						while( waitingBytes > 0 ) {
							byte[] sendBuffer = new byte[chunkSize];
							int sentBytes = inFromFile.read( sendBuffer );

							String encodedData = Base64.encodeBytes( sendBuffer, 0, sentBytes );

							writeLine("<data>"+encodedData+"</data>");

							waitingBytes -= sentBytes;

							rootElement = readXMLLine();
							if( rootElement.getName().equalsIgnoreCase("waiting") ) {
								int remoteWaitingBytes = Integer.parseInt( rootElement.attributeValue("size") );

								if( waitingBytes != remoteWaitingBytes ) {
									writeLine("<receive didFail='false' file='"+fileName+"' size='"+fileSize.toString()+"' />");
									break;
								}
							}else if( rootElement.getName().equalsIgnoreCase("receive") ) {
								if( Boolean.parseBoolean( rootElement.attributeValue("didFail") ) ) {
									retMsg = "Could not upload file '"+fileName+"'.";
									break;
								}else{
									retMsg = "Uploaded file '"+fileName+"'.";
									break;
								}
							}else {
								writeLine("<receive didFail='false' file='"+fileName+"' size='"+fileSize.toString()+"' />");
								return "Malformed server response to download request. (Received '" + rootElement.getName() + "' instead of download response)";
							}
						}

						inFromFile.close();
						sourceFile = null;

						return retMsg;
					}else{
						return "Upload of file '"+fileName+"' failed, remote and local properties mismatch.";
					}
				}else{
					System.out.println(rootElement.asXML());
					return "Upload of file '"+fileName+"' failed, server responded with '"+rootElement.getName()+"' instead of 'waiting'.";
				}
			}catch(Exception e) {
				return "Could not upload '"+fileName+"'. Exception: " + e.getLocalizedMessage();
			}
		}else{
			return "Not connected to remote host";
		}
	}

	public ArrayList<String[]> requestRemoteFileList() {
		if( socket.isConnected() ) {
			try{
				writeLine("<listFiles />");

				Element rootElement = readXMLLine();
				if( rootElement.getName().equalsIgnoreCase("fileList") ) {

					ArrayList<String[]> retList = new ArrayList<String[]>();

					List fileNodes = rootElement.selectNodes("//file");
					for (Object currentNode : fileNodes) {
						Node node = (Node) currentNode;
						retList.add( new String[]{ node.valueOf("@path"), "NEW", node.valueOf("@size"),
							new Date(Long.parseLong(node.valueOf("@lastModified"))).toString() } );
					}
					return retList;

				}
			}catch(Exception e) {}
		}

		return null;
	}

	public Boolean isConnected() {
		if( socket == null) return false;

		return socket.isConnected();
	}

	private void writeLine( String line ) throws IOException {
		if( out != null ) {
			out.write( line );
			out.newLine();
			out.flush();
		}
	}

	private Element readXMLLine() throws IOException, DocumentException {
		if( in == null ) {
			return null;
		}else{
			String response = in.readLine();
			Document xmlDocument = DocumentHelper.parseText( response );
			return xmlDocument.getRootElement();
		}
	}

	private String readXMLResponseResult( Element rootElement ) throws IOException, DocumentException {
		if( rootElement == null ) rootElement = readXMLLine();

		if( rootElement == null ) {
			return null;
		}else{
			return rootElement.attributeValue("result");
		}
	}

}
